package com.erbantou.filemanage.service;

import com.erbantou.filemanage.entity.multipart.DecodedMultipartFile;
import com.erbantou.filemanage.entity.websocket.ProgressMessage;
import com.erbantou.filemanage.entity.websocket.WebSocketConstants;
import com.erbantou.filemanage.util.FileUtils;
import io.minio.messages.Part;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

/**
 * ResourceService 提供了文件资源的管理操作
 * 包括列出资源、上传文件夹、下载文件夹以及删除资源等功能
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ResourceService {

    private final FileHashService fileHashService;
    private final StorageService storageService;
    private final SimpMessagingTemplate messagingTemplate;
    @Value("${custom.file.remove-old-files:true}")
    private boolean removeOldFiles;

    /**
     * 列出所有资源对象
     *
     * @return 包含资源对象名称的列表
     * @throws Exception 如果获取资源列表失败
     */
    public List<String> listResources() throws Exception {
        return storageService.listObjects();
    }

    /**
     * 列出所有资源对象
     *
     * @param prefix 资源对象名称的前缀
     * @return 包含资源对象名称的列表
     * @throws Exception 如果获取资源列表失败
     */
    public List<String> listResources(String prefix) throws Exception {
        return storageService.listObjects(prefix);
    }

    /**
     * 上传文件夹及其包含的文件到 MinIO 存储
     *
     * @param folderName     文件夹名称
     * @param files          包含在文件夹中的文件列表
     * @param requestId      请求 ID
     * @param removeOldFiles 是否删除旧文件
     * @throws Exception 如果上传文件夹失败
     */
    public void uploadFolder(String folderName, List<MultipartFile> files, String requestId, Boolean removeOldFiles) throws Exception {
        if (files.isEmpty()) {
            throw new IllegalArgumentException("没有选择要上传的文件");
        }

        if (removeOldFiles != null) {
            this.removeOldFiles = removeOldFiles;
        }

        log.info("开始上传文件，请求 ID：{}", requestId);
        long startTime = System.currentTimeMillis();

        // 接收到的 files 对象如果来自 Windows 客户端，需要解码
        files = decodeFilenames(files);

        folderName = folderName.trim();
        String listFolderName = folderName.isBlank() ? extractFolderNameFromFiles(files) : folderName;

        long totalSize = calculateTotalSize(files);
        AtomicLong uploadedSize = new AtomicLong(0);

        Set<String> existingFiles = fetchExistingFiles(listFolderName);
        int totalFiles = files.size();

        int fileIndex = 0;
        for (MultipartFile file : files) {
            fileIndex++;
            String objectName = constructObjectName(folderName, file.getOriginalFilename());
            uploadSingleFile(requestId, objectName, file, fileIndex, totalFiles, totalSize, uploadedSize);
            existingFiles.remove(objectName);
        }

        if (Boolean.TRUE.equals(removeOldFiles)) {
            deleteOldFiles(existingFiles);
        }
        logDuration("文件上传完成", startTime);
    }

    /**
     * 下载指定文件夹及其包含的文件为 ZIP 格式
     *
     * @param folderName 文件夹名称
     * @return ZIP 文件流
     * @throws Exception 如果下载文件夹失败
     */
    public InputStream downloadResource(String folderName) throws Exception {
        String prefix = folderName.trim() + "/";
        List<String> objectNames = storageService.listObjects(prefix, true);
        if (objectNames.isEmpty()) {
            throw new Exception("文件夹内没有找到任何文件：" + folderName);
        }
        return storageService.getObjectAsZipStream(objectNames, prefix);
    }

    /**
     * 下载指定文件
     *
     * @param fileName 文件名称
     * @return 文件流
     * @throws Exception 如果下载文件失败
     */
    public InputStream downloadFile(String fileName) throws Exception {
        return storageService.getObject(fileName);
    }

    /**
     * 删除指定的资源对象
     *
     * @param objectName 要删除的资源对象名称
     * @throws Exception 如果删除资源失败
     */
    public void deleteResource(String objectName) throws Exception {
        storageService.removeObjects(objectName);
    }

    /**
     * 如果文件存在于 Minio，获取缓存的文件哈希值信息
     * 文件不存在则返回 null
     *
     * @param objectName 文件名称
     * @return 缓存中的哈希信息或 null
     */
    public String getFileHashIfExist(String objectName) {
        if (storageService.isObjectExist(objectName)) {
            return fileHashService.getFileHash(objectName);
        }
        return null;
    }

    /**
     * 镜像同步删除处理
     *
     * @param objectNames 文件名称列表
     * @return 删除结果信息
     */
    public String mirrorClean(List<String> objectNames) {
        if (objectNames == null || objectNames.isEmpty()) {
            return "没有选择要上传的文件，将会删除所有文件，拒绝本次请求，如需删除请使用删除功能！";
        }

        long startTime = System.currentTimeMillis();
        // 接收到的 objectNames 对象如果来自 Windows 客户端，需要解码
        objectNames = decodeObjectNames(objectNames);

        String listFolderName = extractFolderNameFromObjectNames(objectNames);

        try {
            Set<String> existingFiles = fetchExistingFiles(listFolderName);
            for (String objectName : objectNames) {
                existingFiles.remove(objectName);
            }

            deleteOldFiles(existingFiles);
        } catch (Exception e) {
            return "镜像同步清理失败，请联系管理员！";
        }
        logDuration("镜像同步清理完成", startTime);

        return "镜像同步清理成功！";
    }

    /**
     * 解码文件名
     *
     * @param files 原始文件列表
     */
    private List<MultipartFile> decodeFilenames(List<MultipartFile> files) {
        return files.stream().map(file -> {
            String originalFilename = file.getOriginalFilename();
            if (originalFilename != null) {
                String decodedFilename = URLDecoder.decode(originalFilename, StandardCharsets.UTF_8);
                return new DecodedMultipartFile(file, decodedFilename);
            }
            return file;
        }).toList();
    }

    /**
     * 解码文件名
     *
     * @param objectNames 原始文件名列表
     */
    private List<String> decodeObjectNames(List<String> objectNames) {
        return objectNames.stream()
                .map(o -> URLDecoder.decode(o, StandardCharsets.UTF_8)).toList();
    }

    /**
     * 从文件列表中提取文件夹名称
     *
     * @param files 文件列表
     * @return 文件夹名称
     */
    private String extractFolderNameFromFiles(List<MultipartFile> files) {
        String fileName = files.get(0).getOriginalFilename();
        // 如果文件名中包含斜杠，则返回第一个斜杠之前的字符串部分
        int end = fileName.indexOf("/");
        if (end == -1) {
            return fileName;
        }
        return fileName.substring(0, end);
    }

    /**
     * 从文件名称列表中提取文件夹名称
     *
     * @param objectNames 文件名称列表
     * @return 文件夹名称
     */
    private String extractFolderNameFromObjectNames(List<String> objectNames) {
        String objectName = objectNames.get(0);
        // 如果文件名中包含斜杠，则返回第一个斜杠之前的字符串部分
        int end = objectName.indexOf("/");
        if (end == -1) {
            return objectName;
        }
        return objectName.substring(0, end);
    }

    /**
     * 计算文件列表的总大小
     *
     * @param files 文件列表
     * @return 总大小
     */
    private long calculateTotalSize(List<MultipartFile> files) {
        return files.stream().mapToLong(MultipartFile::getSize).sum();
    }

    /**
     * 获取指定文件夹中的现有文件
     *
     * @param folderName 文件夹名称
     * @return 现有文件的集合
     * @throws Exception 如果获取现有文件失败
     */
    private Set<String> fetchExistingFiles(String folderName) throws Exception {
        return new HashSet<>(storageService.listObjects(folderName, true));
    }

    /**
     * 构造对象名称
     *
     * @param folderName       文件夹名称
     * @param originalFilename 原始文件名
     * @return 对象名称
     */
    private String constructObjectName(String folderName, String originalFilename) {
        return folderName + (folderName.isBlank() ? "" : "/") + originalFilename;
    }

    /**
     * 上传单个文件
     *
     * @param requestId    请求 ID
     * @param objectName   对象名称
     * @param file         要上传的文件
     * @param fileIndex    文件索引
     * @param totalFiles   文件总数
     * @param totalSize    文件总大小
     * @param uploadedSize 已上传文件大小的原子计数器
     * @throws Exception 如果上传文件失败
     */
    private void uploadSingleFile(String requestId, String objectName, MultipartFile file, int fileIndex, int totalFiles, long totalSize, AtomicLong uploadedSize) throws Exception {
        long startTime = System.currentTimeMillis();
        boolean fileExists = storageService.isObjectExist(objectName);
        long fileSize = file.getSize();
        String hrSize = FileUtils.convertToHumanReadableSize(fileSize);

        try (InputStream inputStream = file.getInputStream()) {
            if (fileExists && !isFileChanged(objectName, file)) {
                log.info(String.format("文件内容没有变化，文件大小：%s，跳过 %d/%d：%s", hrSize, fileIndex, totalFiles, objectName));
                updateProgressForSkippedFile(objectName, requestId, totalSize, uploadedSize, fileSize);
            } else {
                log.info(String.format("文件%s，文件大小：%s，重新上传 %d/%d：%s", fileExists ? "内容有变化" : "不存在", hrSize, fileIndex, totalFiles, objectName));
                uploadFileInChunks(requestId, objectName, file, totalSize, uploadedSize);
                fileHashService.storeFileHash(objectName, calculateHash(inputStream));
            }
        }

        logDuration(String.format("处理文件：%s", objectName), startTime);
    }

    /**
     * 分块上传文件
     *
     * @param requestId     请求 ID
     * @param objectName    对象名称
     * @param multipartFile 要上传的文件
     * @param totalSize     文件总大小
     * @param uploadedSize  已上传文件大小的原子计数器
     * @throws Exception 如果上传文件失败
     */
    private void uploadFileInChunks(String requestId, String objectName, MultipartFile multipartFile, long totalSize, AtomicLong uploadedSize) throws Exception {
        long fileSize = multipartFile.getSize();
        int partSize = determinePartSize(fileSize);

        try {
            String contentType = multipartFile.getContentType();
            Part[] parts = new Part[10000];
            int partCount = (int) Math.ceil((double) fileSize / partSize);
            BufferedInputStream data = new BufferedInputStream(multipartFile.getInputStream());
            String uploadId = storageService.initMultiPartUpload(objectName, data, partSize, contentType);

            long fileUploadedSize = 0L;
            for (int partNumber = 1; partNumber <= partCount || partCount < 0; ++partNumber) {
                fileUploadedSize = storageService.uploadFilePart(objectName, data, contentType, fileSize, fileUploadedSize, partCount, partSize, parts, partNumber, uploadId);
                updateProgress(objectName, requestId, totalSize, uploadedSize, partNumber == partCount ? (int) (fileSize % partSize) : partSize);
                log.debug(String.format("上传文件块 %d/%d 已上传文件大小 %s", partNumber, partCount, FileUtils.convertToHumanReadableSize(fileUploadedSize)));
            }

            if (partCount > 1) {
                storageService.completeMultipartUpload(objectName, parts, uploadId);
            }
        } catch (Exception e) {
            log.error("上传文件失败", e);
            throw e;
        }
    }

    /**
     * 确定上传文件的分块大小
     *
     * @param fileSize 文件大小
     * @return 分块大小
     */
    private int determinePartSize(long fileSize) {
        if (fileSize > 500 * 1024 * 1024) { // 大于500MB
            return 20 * 1024 * 1024; // 20MB
        } else if (fileSize > 100 * 1024 * 1024) { // 大于100MB
            return 10 * 1024 * 1024; // 10MB
        } else {
            return 5 * 1024 * 1024; // 5MB
        }
    }

    /**
     * 更新上传进度
     *
     * @param objectName   对象名称
     * @param requestId    请求 ID
     * @param totalSize    文件总大小
     * @param uploadedSize 已上传文件大小的原子计数器
     * @param partSize     当前上传的分块大小
     */
    private void updateProgress(String objectName, String requestId, long totalSize, AtomicLong uploadedSize, int partSize) {
        long newUploadedSize = uploadedSize.addAndGet(partSize);
        double progress = (newUploadedSize / (double) totalSize) * 100;
        ProgressMessage progressMessage = new ProgressMessage(objectName, requestId, progress);
        messagingTemplate.convertAndSend(WebSocketConstants.TOPIC_UPLOAD_PROGRESS, progressMessage);
    }

    /**
     * 更新跳过文件的上传进度
     *
     * @param objectName   对象名称
     * @param requestId    请求 ID
     * @param totalSize    文件总大小
     * @param uploadedSize 已上传文件大小的原子计数器
     * @param fileSize     文件大小
     */
    private void updateProgressForSkippedFile(String objectName, String requestId, long totalSize, AtomicLong uploadedSize, long fileSize) {
        long newUploadedSize = uploadedSize.addAndGet(fileSize);
        double progress = (newUploadedSize / (double) totalSize) * 100;
        ProgressMessage progressMessage = new ProgressMessage(objectName, requestId, progress);
        messagingTemplate.convertAndSend(WebSocketConstants.TOPIC_UPLOAD_PROGRESS, progressMessage);
    }

    /**
     * 检查文件是否发生变化
     *
     * @param objectName 对象名称
     * @param file       要检查的文件
     * @return true 如果文件发生变化，false 否则
     * @throws IOException              如果读取文件失败
     * @throws NoSuchAlgorithmException 如果计算哈希值失败
     */
    private boolean isFileChanged(String objectName, MultipartFile file) throws IOException, NoSuchAlgorithmException {
        String existingFileHash = fileHashService.getFileHash(objectName);
        String newFileHashString = calculateHash(file.getInputStream());
        return existingFileHash == null || !existingFileHash.equals(newFileHashString);
    }

    /**
     * 计算文件的哈希值
     *
     * @param inputStream 文件输入流
     * @return 文件的哈希值
     * @throws IOException 如果读取文件失败
     */
    private String calculateHash(InputStream inputStream) throws IOException {
        return DigestUtils.sha256Hex(inputStream);
    }

    /**
     * 删除旧文件
     *
     * @param existingFiles 要删除的旧文件集合
     * @throws Exception 如果删除文件失败
     */
    private void deleteOldFiles(Set<String> existingFiles) throws Exception {
        log.info("开始删除旧文件");
        for (String oldFile : existingFiles) {
            log.debug(String.format("删除旧文件：%s", oldFile));
            storageService.removeObject(oldFile);
        }
        log.info("删除旧文件完成");
    }

    /**
     * 记录操作的执行时间
     *
     * @param message   日志信息
     * @param startTime 操作开始时间
     */
    private void logDuration(String message, long startTime) {
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        log.info(String.format("%s，用时：%d 毫秒", message, duration));
    }
}
